# install.packages("pak")
pak::pak("adamoshen/lcpizza")February 17, 2026
Let's set the scene:
I've just finished work. I'm exhausted 😵 and I don't want to prepare my own dinner.
Takeout or pick-up over dine-in or delivery because I don't want to be paying extra tips or service charges 😒.
I've been eating a lot of 🐔 chicken and 🥬 leafy salads 🥗 lately, and I want something with a different flavour profile.
💪🏼 I'm trying to hit my daily protein goals while also making sure that I'm not going too overboard with the calories (a little over is okay).
Skipping meals is 🙅🏼♂️ when you're trying to make 💪🏼 gainsss 🦵🏼.
The food needs to be ready reasonably fast so that I have enough time to digest it before heading to the gym 🏋🏼♂️ later in the evening.
Why?
Their sauce is 👌🏼 perfectly 👌🏼 seasoned.
Their dough is also perfect: crispy on the outside and soft and airy on the inside. (Unlike Domino's where the crust is super thick and dense 😒)
Their items are reasonably priced 🫰🏼 for the amount of food you get and there are often discounts on the app or on social media 🤑.
Leftovers reheat well so you can eat it again tomorrow 🤤. See this tip (Instagram).
Fine print: Max. 10 per transaction!
Of their "core" pizza offerings, which pizza is the 👍🏼💰 most cost effective while having the ⬇️🫃🏼 least calories and the ⬆️💪🏼 most protein? (We're going to ignore fat, cholesterol, sodium, etc.)
Does this change under the conditions of either of these deals:
Tip
Recall that (in most cases) a food is considered "high-protein" if its protein content (in grams) is at least one-tenth of its calories. For example, a food with 100 calories should have at least 10g of protein.
As of February 2026, the price of Fairlife Nutrition Plan shakes has increased to $49.99 for a case of 18 at Costco.
Each bottle has 150 calories and 30g of protein (high protein).
This works out to be $2.78 per bottle, or $0.09267 per gram of protein. Calorie-to-protein ratio is 5 (lower is better).
To aid me in this analysis, I recently took the time to collect the nutrition and local pricing data and bundle it up as the R package, lcpizza!
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.2.0 ✔ readr 2.1.6
✔ forcats 1.0.1 ✔ stringr 1.6.0
✔ ggplot2 4.0.2 ✔ tibble 3.3.1
✔ lubridate 1.9.5 ✔ tidyr 1.3.2
✔ purrr 1.2.1
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
lcpizza# A tibble: 27 × 18
size crust flavour price calories total_fat sat_fat trans_fat cholesterol
<chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 medium regular Pepper… 7.99 1590 63 29 1.5 125
2 medium regular Cheese 7.99 1400 46 22 0.5 85
3 medium regular Ultima… 11.5 1750 74 33 1.5 150
4 medium regular 3 Meat… 11.5 1980 98 41 1.5 205
5 medium regular Canadi… 11.5 1820 84 36 1.5 175
6 medium regular Hula H… 11.5 1590 49 23 0.5 125
7 medium regular Veggie 12.5 1520 53 23 0.5 85
8 medium regular BBQ Ch… 12.5 1550 48 23 0.5 130
9 medium stuffed Cheese 12.5 1760 75 39 1.5 165
10 medium stuffed Pepper… 12.5 1960 93 46 2.5 205
# ℹ 17 more rows
# ℹ 9 more variables: sodium <dbl>, total_carb <dbl>, fibre <dbl>, sugar <dbl>,
# protein <dbl>, vit_a <dbl>, vit_c <dbl>, calcium <dbl>, iron <dbl>
lcpizza# A tibble: 32 × 17
size topping price calories total_fat sat_fat trans_fat cholesterol sodium
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 medium peppero… 2 260 23 9 1 55 1140
2 medium pineapp… 2 80 0 0 0 0 0
3 medium ham 2 80 2.5 1 0 35 910
4 medium mushroo… 2 10 0 0 0 0 0
5 medium bacon 2 280 26 9 0 65 990
6 medium green p… 2 25 0 0 0 0 0
7 medium onions 2 25 0 0 0 0 0
8 medium sausage 2 230 20 7 0 40 720
9 medium chicken 2.5 80 1.5 0 0 40 490
10 medium red oni… 2 25 0 0 0 0 0
# ℹ 22 more rows
# ℹ 8 more variables: total_carb <dbl>, fibre <dbl>, sugar <dbl>,
# protein <dbl>, vit_a <dbl>, vit_c <dbl>, calcium <dbl>, iron <dbl>
Let's first focus on the medium cheese, medium pepperoni, large cheese, and large pepperoni pizzas.
# A tibble: 5 × 6
name price calories protein price_per_g_protein calorie_protein_ratio
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Medium Peppe… 7.99 1590 75 0.107 21.2
2 Medium Cheese 7.99 1400 66 0.121 21.2
3 Large Pepper… 13.5 2160 105 0.128 20.6
4 Large Cheese 13.5 1920 94 0.144 20.4
5 Fairlife Pro… 2.78 150 30 0.0926 5
basic_discounts <- tibble(
name = c("Large Pepperoni", "Large Cheese"),
price = 9.99
)
basic_comparison_with_discounts <- basic_pizzas %>%
mutate(size = str_to_title(size)) %>%
unite(name, size, flavour, sep = " ") %>%
bind_rows(fairlife) %>%
rows_update(basic_discounts, by = "name") %>%
bind_metrics() %>%
select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio)Without discount:
# A tibble: 5 × 6
name price calories protein price_per_g_protein calorie_protein_ratio
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Medium Peppe… 7.99 1590 75 0.107 21.2
2 Medium Cheese 7.99 1400 66 0.121 21.2
3 Large Pepper… 13.5 2160 105 0.128 20.6
4 Large Cheese 13.5 1920 94 0.144 20.4
5 Fairlife Pro… 2.78 150 30 0.0926 5
With discount:
# A tibble: 5 × 6
name price calories protein price_per_g_protein calorie_protein_ratio
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Medium Peppe… 7.99 1590 75 0.107 21.2
2 Medium Cheese 7.99 1400 66 0.121 21.2
3 Large Pepper… 9.99 2160 105 0.0951 20.6
4 Large Cheese 9.99 1920 94 0.106 20.4
5 Fairlife Pro… 2.78 150 30 0.0926 5
The discount makes the Large Pepperoni pizza comparable to the Fairlife Shake in Price per Grams of Protein 🤯!
# A tibble: 12 × 5
size flavour price calories protein
<chr> <chr> <dbl> <dbl> <dbl>
1 medium Ultimate Supreme 11.5 1750 83
2 medium 3 Meat Treat 11.5 1980 90
3 medium Canadian 11.5 1820 83
4 medium Hula Hawaiian 11.5 1590 83
5 medium Veggie 12.5 1520 70
6 medium BBQ Chicken 12.5 1550 84
7 large Ultimate Supreme 16.5 2390 115
8 large 3 Meat Treat 16.5 2560 119
9 large Hula Hawaiian 16.5 2160 115
10 large Canadian 17.0 2460 115
11 large Veggie 17.5 2070 100
12 large BBQ Chicken 18.5 2110 120
# A tibble: 13 × 6
name price calories protein price_per_g_protein calorie_protein_ratio
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Medium Ulti… 11.5 1750 83 0.138 21.1
2 Medium 3 Me… 11.5 1980 90 0.128 22
3 Medium Cana… 11.5 1820 83 0.138 21.9
4 Medium Hula… 11.5 1590 83 0.138 19.2
5 Medium Vegg… 12.5 1520 70 0.178 21.7
6 Medium BBQ … 12.5 1550 84 0.149 18.5
7 Large Ultim… 16.5 2390 115 0.143 20.8
8 Large 3 Mea… 16.5 2560 119 0.139 21.5
9 Large Hula … 16.5 2160 115 0.143 18.8
10 Large Canad… 17.0 2460 115 0.148 21.4
11 Large Veggie 17.5 2070 100 0.175 20.7
12 Large BBQ C… 18.5 2110 120 0.154 17.6
13 Fairlife Pr… 2.78 150 30 0.0926 5
specialty_discounts <- tibble(
name = c("Large 3 Meat Treat", "Large Canadian", "Large Ultimate Supreme", "Large Hula Hawaiian"),
price = 13.99
)
specialty_comparison_with_discounts <- specialty_pizzas %>%
mutate(size = str_to_title(size)) %>%
unite(name, size, flavour, sep = " ") %>%
bind_rows(fairlife) %>%
rows_update(specialty_discounts, by = "name") %>%
bind_metrics() %>%
select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio)Note: The Large Veggie and Large BBQ Chicken are excluded from these discounts!
Without discounts, the Medium specialty pizzas actually have a lower Price Per Gram of Protein! Unsurprisingly, the Hula Hawaiian and the BBQ Chicken have the lowest Calorie to Protein Ratios (after the Fairlife Shake). I'm not really a fan of the BBQ Chicken pizza though 🫤.
With discounts, the Large specialty pizzas have a better Price Per Gram of Protein than their Medium counterparts and get closer to the Fairlife Shake. The Large Hawaiian pizza excels over both metrics!
large_pizzas <- pizzas %>%
filter(size == "large", crust == "regular") %>%
mutate(size = str_to_title(size)) %>%
unite(name, size, flavour, sep = " ") %>%
bind_rows(fairlife) %>%
bind_metrics() %>%
select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio) %>%
mutate(bar_fill = if_else(str_detect(name, "Fairlife"), "#9C755F", "#F28E2B"))large_ppgp <- large_pizzas %>%
slice_min(price_per_g_protein, n = 7) %>%
mutate(
name = fct_reorder(name, price_per_g_protein),
price_label = scales::number(price_per_g_protein, accuracy = 0.0001, prefix = "$")
) %>%
ggplot(aes(x = price_per_g_protein, y = name)) +
geom_col(aes(fill = bar_fill), show.legend = FALSE) +
geom_text(aes(label = price_label), nudge_x = 0.03) +
scale_fill_identity() +
theme_minimal() +
theme_sub_axis_x(text = element_blank()) +
theme_sub_axis_y(text = element_text(size = 12)) +
theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
coord_cartesian(clip = "off") +
labs(y = "", x = "Price per gram of protein (lower is better)")
large_cpr <- large_pizzas %>%
slice_min(calorie_protein_ratio, n = 7) %>%
mutate(
name = fct_reorder(name, calorie_protein_ratio),
ratio_label = scales::number(calorie_protein_ratio, accuracy = 0.01)
) %>%
ggplot(aes(x = calorie_protein_ratio, y = name)) +
geom_col(aes(fill = bar_fill), show.legend = FALSE) +
geom_text(aes(label = ratio_label), nudge_x = 3) +
scale_fill_identity() +
theme_minimal() +
theme_sub_axis_x(text = element_blank()) +
theme_sub_axis_y(text = element_text(size = 12)) +
theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
coord_cartesian(clip = "off") +
labs(y = "", x = "Calorie to protein ratio (lower is better)")
large_ppgp + large_cprlarge_pizzas_with_discount <- pizzas %>%
filter(size == "large", crust == "regular") %>%
mutate(size = str_to_title(size)) %>%
unite(name, size, flavour, sep = " ") %>%
rows_update(basic_discounts, by = "name") %>%
rows_update(specialty_discounts, by = "name") %>%
bind_rows(fairlife) %>%
bind_metrics() %>%
select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio) %>%
mutate(bar_fill = if_else(str_detect(name, "Fairlife"), "#9C755F", "#F28E2B"))large_ppgp_discount <- large_pizzas_with_discount %>%
slice_min(price_per_g_protein, n = 7) %>%
mutate(
name = fct_reorder(name, price_per_g_protein),
price_label = scales::number(price_per_g_protein, accuracy = 0.0001, prefix = "$")
) %>%
ggplot(aes(x = price_per_g_protein, y = name)) +
geom_col(aes(fill = bar_fill), show.legend = FALSE) +
geom_text(aes(label = price_label), nudge_x = 0.03) +
scale_fill_identity() +
theme_minimal() +
theme_sub_axis_x(text = element_blank()) +
theme_sub_axis_y(text = element_text(size = 12)) +
theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
coord_cartesian(clip = "off") +
labs(y = "", x = "Price per gram of protein (lower is better)")
large_cpr_discount <- large_pizzas_with_discount %>%
slice_min(calorie_protein_ratio, n = 7) %>%
mutate(
name = fct_reorder(name, calorie_protein_ratio),
ratio_label = scales::number(calorie_protein_ratio, accuracy = 0.01)
) %>%
ggplot(aes(x = calorie_protein_ratio, y = name)) +
geom_col(aes(fill = bar_fill), show.legend = FALSE) +
geom_text(aes(label = ratio_label), nudge_x = 3) +
scale_fill_identity() +
theme_minimal() +
theme_sub_axis_x(text = element_blank()) +
theme_sub_axis_y(text = element_text(size = 12)) +
theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
coord_cartesian(clip = "off") +
labs(y = "", x = "Calorie to protein ratio (lower is better)")
large_ppgp_discount + large_cpr_discountRather than looking at price per gram of protein, it probably would have been better to look at the price per 30g of protein to get an idea of how much you would be paying for pizza compared to a bottle of the Fairlife protein shake.
large_pizzas2 <- pizzas %>%
filter(size == "large", crust == "regular") %>%
mutate(size = str_to_title(size)) %>%
unite(name, size, flavour, sep = " ") %>%
bind_rows(fairlife) %>%
bind_metrics2() %>%
select(name, price, calories, protein, price_per_30g_protein, calorie_protein_ratio) %>%
mutate(bar_fill = if_else(str_detect(name, "Fairlife"), "#9C755F", "#F28E2B"))large_ppgp2 <- large_pizzas2 %>%
slice_min(price_per_30g_protein, n = 7) %>%
mutate(
name = fct_reorder(name, price_per_30g_protein),
price_label = scales::number(price_per_30g_protein, accuracy = 0.01, prefix = "$")
) %>%
ggplot(aes(x = price_per_30g_protein, y = name)) +
geom_col(aes(fill = bar_fill), show.legend = FALSE) +
geom_text(aes(label = price_label), nudge_x = 0.60) +
scale_fill_identity() +
theme_minimal() +
theme_sub_axis_x(text = element_blank()) +
theme_sub_axis_y(text = element_text(size = 12)) +
theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
coord_cartesian(clip = "off") +
labs(y = "", x = "Price per 30 grams of protein (lower is better)")
large_cpr2 <- large_pizzas2 %>%
slice_min(calorie_protein_ratio, n = 7) %>%
mutate(
name = fct_reorder(name, calorie_protein_ratio),
ratio_label = scales::number(calorie_protein_ratio, accuracy = 0.01)
) %>%
ggplot(aes(x = calorie_protein_ratio, y = name)) +
geom_col(aes(fill = bar_fill), show.legend = FALSE) +
geom_text(aes(label = ratio_label), nudge_x = 3) +
scale_fill_identity() +
theme_minimal() +
theme_sub_axis_x(text = element_blank()) +
theme_sub_axis_y(text = element_text(size = 12)) +
theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
coord_cartesian(clip = "off") +
labs(y = "", x = "Calorie to protein ratio (lower is better)")
large_ppgp2 + large_cpr2large_pizzas2_with_discount <- pizzas %>%
filter(size == "large", crust == "regular") %>%
mutate(size = str_to_title(size)) %>%
unite(name, size, flavour, sep = " ") %>%
rows_update(basic_discounts, by = "name") %>%
rows_update(specialty_discounts, by = "name") %>%
bind_rows(fairlife) %>%
bind_metrics2() %>%
select(name, price, calories, protein, price_per_30g_protein, calorie_protein_ratio) %>%
mutate(bar_fill = if_else(str_detect(name, "Fairlife"), "#9C755F", "#F28E2B"))large_ppgp_discount2 <- large_pizzas2_with_discount %>%
slice_min(price_per_30g_protein, n = 7) %>%
mutate(
name = fct_reorder(name, price_per_30g_protein),
price_label = scales::number(price_per_30g_protein, accuracy = 0.01, prefix = "$")
) %>%
ggplot(aes(x = price_per_30g_protein, y = name)) +
geom_col(aes(fill = bar_fill), show.legend = FALSE) +
geom_text(aes(label = price_label), nudge_x = 0.60) +
scale_fill_identity() +
theme_minimal() +
theme_sub_axis_x(text = element_blank()) +
theme_sub_axis_y(text = element_text(size = 12)) +
theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
coord_cartesian(clip = "off") +
labs(y = "", x = "Price per 30 grams of protein (lower is better)")
large_cpr_discount2 <- large_pizzas2_with_discount %>%
slice_min(calorie_protein_ratio, n = 7) %>%
mutate(
name = fct_reorder(name, calorie_protein_ratio),
ratio_label = scales::number(calorie_protein_ratio, accuracy = 0.01)
) %>%
ggplot(aes(x = calorie_protein_ratio, y = name)) +
geom_col(aes(fill = bar_fill), show.legend = FALSE) +
geom_text(aes(label = ratio_label), nudge_x = 3) +
scale_fill_identity() +
theme_minimal() +
theme_sub_axis_x(text = element_blank()) +
theme_sub_axis_y(text = element_text(size = 12)) +
theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
coord_cartesian(clip = "off") +
labs(y = "", x = "Calorie to protein ratio (lower is better)")
large_ppgp_discount2 + large_cpr_discount2With or without discounts, the Large BBQ Chicken and Large Hula Hawaiian pizzas are slightly more "health conscious" 😏.
With discounts, the Large Pepperoni is comparable to the Fairlife Protein Shake in price per gram of protein. However, the Large Hawaiian is not too far off!
Therefore, if you are trying to make gains, save money, and let loose once in a while, the Large Hawaiian pizza is the way to go.
Alternative conclusion: 🍍 Pineapple 🍍 belongs on pizza 😎